﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using LinqToOwin;

namespace Microsoft.Owin
{
  public static class OwinContextExtensions
  {
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Usage is consistent with other instance methods.")]
    public static object NoResponse(this IOwinContext context)
    {
      Contract.Requires(context != null);
      Contract.Ensures(Contract.Result<object>() == null);

      return null;
    }

    private static Tuple<PathString, bool> ResolveAppRelative(this IOwinContext context, string path)
    {
      if (path.StartsWith("~/"))
      {
        var root = context.Request.PathBase;

        if (root == PathString.Empty)
        {
          root = new PathString("/");
        }

        return Tuple.Create(path.Length > 2 ? root.Add(new PathString(path.Substring(2))) : root, true);
      }
      else
      {
        return Tuple.Create(new PathString(path), false);
      }
    }

    public static PathString GetResponseBasedPath(this IOwinContext context, string path)
    {
      var resolved = context.ResolveAppRelative(path);

      return resolved.Item2 ? resolved.Item1 : context.GetResponseBasedPath(resolved.Item1);
    }

    public static PathString GetResponseBasedPath(this IOwinContext context, PathString path)
    {
      var based = context as IResponsePathBase;

      return based == null ? path : based.ResponsePathBase.Add(path);
    }

    public static IEnumerable<PathString> GetResponseBasedPaths(this IOwinContext context, params string[] paths)
    {
      return context.GetResponseBasedPaths((IEnumerable<string>)paths);
    }

    public static IEnumerable<PathString> GetResponseBasedPaths(this IOwinContext context, IEnumerable<string> paths)
    {
      return from path in paths
             select context.GetResponseBasedPath(path);
    }

    public static IEnumerable<PathString> GetResponseBasedPaths(this IOwinContext context, params PathString[] paths)
    {
      return context.GetResponseBasedPaths((IEnumerable<PathString>)paths);
    }

    public static IEnumerable<PathString> GetResponseBasedPaths(this IOwinContext context, IEnumerable<PathString> paths)
    {
      var based = context as IResponsePathBase;

      return based == null ? paths : from path in paths
                                     select based.ResponsePathBase.Add(path);
    }

    public static bool IsRequestForAny(this IOwinContext context, params string[] paths)
    {
      Contract.Requires(context != null);

      return context.IsRequestForAny((IEnumerable<string>)paths);
    }

    public static bool IsRequestForAny(this IOwinContext context, IEnumerable<string> paths)
    {
      Contract.Requires(context != null);

      return context.IsRequestForAny(from path in paths
                                     select new PathString(path));
    }

    public static bool IsRequestForAny(this IOwinContext context, params PathString[] paths)
    {
      Contract.Requires(context != null);

      return context.IsRequestForAny((IEnumerable<PathString>)paths);
    }

    public static bool IsRequestForAny(this IOwinContext context, IEnumerable<PathString> paths)
    {
      Contract.Requires(context != null);

      return context.GetResponseBasedPaths(paths).Any(context.Request.Path.StartsWithSegments);
    }

    public static TContext Do<TContext>(this TContext context, Action action)
      where TContext : IOwinContext
    {
      Contract.Requires(context != null);
      Contract.Requires(action != null);
      Contract.Ensures(object.Equals(Contract.Result<TContext>(), context));

      action();

      return context;
    }

    public static Task<TContext> DoAsync<TContext>(this TContext context, Action action)
      where TContext : IOwinContext
    {
      Contract.Requires(context != null);
      Contract.Requires(action != null);
      Contract.Ensures(Contract.Result<Task<TContext>>() != null);
      Contract.Ensures(Contract.Result<Task<TContext>>().IsCompleted);
      Contract.Ensures(object.Equals(Contract.Result<Task<TContext>>().Result, context));

      action();

      return Task.FromResult(context);
    }

    public static async Task<IOwinContext> DoAsync(this IOwinContext context, Func<Task> action)
    {
      Contract.Requires(context != null);
      Contract.Requires(action != null);

      await action().ConfigureAwait(false);

      return context;
    }

    public static async Task<IOwinContext> DoAsync<T>(this IOwinContext context, Func<Task<T>> action)
    {
      Contract.Requires(context != null);
      Contract.Requires(action != null);

      await action().ConfigureAwait(false);

      return context;
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Usage is consistent with other instance methods.")]
    public static T Respond<T>(this IOwinContext context, Func<T> function)
    {
      Contract.Requires(context != null);
      Contract.Requires(function != null);

      return function();
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Usage is consistent with other instance methods.")]
    public static T Respond<T>(this IOwinContext context, T value)
    {
      Contract.Requires(context != null);

      return value;
    }

    public static object Redirect(this IOwinContext context, string location)
    {
      Contract.Requires(context != null);
      Contract.Ensures(Contract.Result<object>() == null);

      var path = context.GetResponseBasedPath(location).Value;

      context.Response.Redirect(path);

      return null;
    }

    public static TContext Status<TContext>(this TContext context, HttpStatusCode code)
      where TContext : IOwinContext
    {
      Contract.Requires(context != null);
      Contract.Ensures(object.Equals(Contract.Result<TContext>(), context));

      context.Response.StatusCode = (int)code;

      return context;
    }

    public static T WithContentType<T>(this IOwinContext context, string contentType, T result)
    {
      Contract.Requires(context != null);
      Contract.Ensures(object.Equals(Contract.Result<T>(), result));

      context.Response.ContentType = contentType;

      return result;
    }

    public static string Status(this IOwinContext context, HttpStatusCode code, string message)
    {
      Contract.Requires(context != null);
      Contract.Ensures(Contract.Result<string>() == message);

      context.Response.StatusCode = (int)code;

      return message;
    }

    public static TContext SignIn<TContext>(this TContext context, string userName, string authenticationType = null, params Claim[] claims)
      where TContext : IOwinContext
    {
      Contract.Requires(context != null);
      Contract.Requires(claims != null);
      Contract.Ensures(object.Equals(Contract.Result<TContext>(), context));

      context.Authentication.SignIn(new ClaimsIdentity(new[]
      {
        new Claim(ClaimTypes.Name, userName)
      }
      .Concat(claims),
      authenticationType));

      return context;
    }

    public static T IfAuthenticated<T>(this IOwinContext context, Func<T> ifTrue, T ifFalse = default(T))
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);

      return context.Authentication.User.Identity.IsAuthenticated ? ifTrue() : ifFalse;
    }

    public static T IfAuthenticated<T>(this IOwinContext context, Func<T> ifTrue, Func<T> ifFalse)
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);
      Contract.Requires(ifFalse != null);

      return context.Authentication.User.Identity.IsAuthenticated ? ifTrue() : ifFalse();
    }

    public static T IfAuthenticatedInRole<T>(this IOwinContext context, string role, Func<T> ifTrue, T ifFalse = default(T))
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);

      return context.Authentication.User.IsInRole(role) ? ifTrue() : ifFalse;
    }

    public static T IfAuthenticatedInRole<T>(this IOwinContext context, string role, Func<T> ifTrue, Func<T> ifFalse)
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);
      Contract.Requires(ifFalse != null);

      return context.Authentication.User.IsInRole(role) ? ifTrue() : ifFalse();
    }

    public static T IfAuthenticated<T>(this IOwinContext context, Func<ClaimsPrincipal, T> ifTrue, T ifFalse = default(T))
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);

      return context.Authentication.User.Identity.IsAuthenticated ? ifTrue(context.Authentication.User) : ifFalse;
    }

    public static T IfAuthenticated<T>(this IOwinContext context, Func<ClaimsPrincipal, T> ifTrue, Func<T> ifFalse)
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);
      Contract.Requires(ifFalse != null);

      return context.Authentication.User.Identity.IsAuthenticated ? ifTrue(context.Authentication.User) : ifFalse();
    }

    public static T IfAuthenticatedInRole<T>(this IOwinContext context, string role, Func<ClaimsPrincipal, T> ifTrue, T ifFalse = default(T))
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);

      return context.Authentication.User.IsInRole(role) ? ifTrue(context.Authentication.User) : ifFalse;
    }

    public static T IfAuthenticatedInRole<T>(this IOwinContext context, string role, Func<ClaimsPrincipal, T> ifTrue, Func<T> ifFalse)
    {
      Contract.Requires(context != null);
      Contract.Requires(ifTrue != null);
      Contract.Requires(ifFalse != null);

      return context.Authentication.User.IsInRole(role) ? ifTrue(context.Authentication.User) : ifFalse();
    }
  }
}